# Copyright (c) 2010 Vladimir Prus.
# Copyright (c) 2013 Steven Watanabe
# Copyright (c) 2021 Rene Ferdinand Rivera Morell
#
# Use, modification and distribution is subject to the Boost Software
# License Version 1.0. (See accompanying file LICENSE.txt or
# https://www.bfgroup.xyz/b2/LICENSE.txt)

import property-set ;
import path ;
import modules ;
import "class" ;
import errors ;
import configure ;
import feature ;
import project ;
import virtual-target ;
import generators ;
import property ;
import print ;
import regex ;

project.initialize $(__name__) ;
.project = [ project.current ] ;
project ac ;

feature.feature ac.print-text : : free ;

rule generate-include ( target : sources * : properties * )
{
    print.output $(target) ;
    local text = [ property.select <ac.print-text> : $(properties) ] ;
    if $(text)
    {
        print.text $(text:G=) : true ;
    }
    else
    {
        local header = [ property.select <include> : $(properties) ] ;
        print.text "#include <$(header:G=)>\n" : true ;
    }
}

rule generate-main ( target : sources * : properties * )
{
    print.output $(target) ;
    print.text "int main() {}" : true ;
}

rule find-include-path ( properties : header : provided-path ? : test-source ? )
{
    if $(provided-path) && [ path.exists [ path.root $(header) $(provided-path) ] ]
    {
        return $(provided-path) ;
    }
    else
    {
        local a = [ class.new action : ac.generate-include : [ property-set.create <include>$(header) <ac.print-text>$(test-source) ] ] ;
        # Create a new CPP target named after the header.
        # Replace dots (".") in target basename for portability.
        local basename = [ regex.replace $(header:D=) "[.]" "_" ] ;
        local header-target = $(header:S=:B=$(basename)) ;
        local cpp = [ class.new file-target $(header-target:S=.cpp) exact : CPP : $(.project) : $(a) ] ;
        cpp = [ virtual-target.register $(cpp) ] ;
        $(cpp).root true ;
        local result = [ generators.construct $(.project) $(header-target) : OBJ : $(properties) : $(cpp) : true ] ;
        configure.maybe-force-rebuild $(result[2-]) ;
        local jam-targets ;
        for local t in $(result[2-])
        {
            jam-targets += [ $(t).actualize ] ;
        }
        if [ UPDATE_NOW $(jam-targets) : [ modules.peek configure : .log-fd ]
            : ignore-minus-n ]
        {
            return %default ;
        }
    }
}

rule construct-library ( name : property-set : provided-path ? )
{
    local lib-props = [ $(property-set).add-raw <name>$(name) <search>$(provided-path) ] ;
    return [ generators.construct $(.project) lib-$(name)
        : SEARCHED_LIB : $(lib-props) : : true ] ;
}


rule find-library ( properties : names + : provided-path ? )
{
    local result ;
    if [ $(properties).get <link> ] = shared
    {
        link-opts = <link>shared <link>static ;
    }
    else
    {
        link-opts = <link>static <link>shared ;
    }
    while $(link-opts)
    {
        local names-iter = $(names) ;
        properties = [ $(properties).refine [ property-set.create $(link-opts[1]) ] ] ;
        while $(names-iter)
        {
            local name = $(names-iter[1]) ;
            local lib = [ construct-library $(name) : $(properties) : $(provided-path) ] ;
            local a = [ class.new action : ac.generate-main :
                [ property-set.empty ] ] ;
            local main.cpp = [ virtual-target.register
                [ class.new file-target main-$(name).cpp exact : CPP : $(.project) : $(a) ] ] ;
            $(main.cpp).root true ;
            local test = [ generators.construct $(.project) $(name) : EXE
                : [ $(properties).add $(lib[1]) ] : $(main.cpp) $(lib[2-])
                : true ] ;
            configure.maybe-force-rebuild $(test[2-]) ;
            local jam-targets ;
            for t in $(test[2-])
            {
                jam-targets += [ $(t).actualize ] ;
            }
            if [ UPDATE_NOW $(jam-targets) : [ modules.peek configure : .log-fd ]
                    : ignore-minus-n ]
            {
                result = $(name) $(link-opts[1]) ;
                names-iter = ; link-opts = ; # break
            }
            names-iter = $(names-iter[2-]) ;
        }
        link-opts = $(link-opts[2-]) ;
    }
    return $(result) ;
}

class ac-library : basic-target
{
    import errors ;
    import indirect ;
    import virtual-target ;
    import ac ;
    import configure ;
    import config-cache ;
    import os ;

    rule __init__ ( name : project : requirements * : include-path ? : library-path ? : library-name ? )
    {
        basic-target.__init__ $(name) : $(project) : : $(requirements) ;

        reconfigure $(include-path) : $(library-path) : $(library-name) ;
    }

    rule set-header ( header )
    {
        self.header = $(header) ;
    }

    rule set-default-names ( names + )
    {
        self.default-names = $(names) ;
    }

    rule set-header-test ( source )
    {
        self.header-test = $(source) ;
    }

    rule reconfigure ( include-path ? : library-path ? : library-name ? )
    {
        if $(include-path) || $(library-path) || $(library-name)
        {
            check-not-configured ;

            self.include-path = $(include-path) ;
            self.library-path = $(library-path) ;
            self.library-name = $(library-name) ;
        }
    }

    rule set-target ( target )
    {
        check-not-configured ;
        self.target = $(target) ;
    }

    rule check-not-configured ( )
    {
        if $(self.include-path) || $(self.library-path) || $(self.library-name) || $(self.target)
        {
            errors.user-error [ name ] "is already configured" ;
        }
    }

    rule construct ( name : sources * : property-set )
    {
        if $(self.target)
        {
            return [ $(self.target).generate $(property-set) ] ;
        }
        else
        {
            local use-environment ;
            if ! $(self.library-name) && ! $(self.include-path) && ! $(self.library-path)
            {
                use-environment = true ;
            }
            local libnames = $(self.library-name) ;
            if ! $(libnames) && $(use-environment)
            {
                libnames = [ os.environ $(name:U)_NAME ] ;
                # Backward compatibility only.
                libnames ?= [ os.environ $(name:U)_BINARY ] ;
            }
            libnames ?= $(self.default-names) ;

            local include-path = $(self.include-path) ;
            if ! $(include-path) && $(use-environment)
            {
                include-path = [ os.environ $(name:U)_INCLUDE ] ;
            }

            local library-path = $(self.library-path) ;
            if ! $(library-path) && $(use-environment)
            {
                library-path = [ os.environ $(name:U)_LIBRARY_PATH ] ;
                # Backwards compatibility only
                library-path ?= [ os.environ $(name:U)_LIBPATH ] ;
            }

            local relevant = [ property.select [ configure.get-relevant-features ] <link> :
                [ $(property-set).raw ] ] ;
            local min = [ property.as-path [ SORT [ feature.minimize $(relevant) ] ] ] ;

            local key = ac-library-$(name)-$(relevant:J=-) ;
            local lookup = [ config-cache.get $(key) ] ;

            if $(lookup)
            {
                if $(lookup) = missing
                {
                    configure.log-library-search-result $(name) : "no  (cached)" $(min) ;
                    return [ property-set.empty ] ;
                }
                else
                {
                    local includes = $(lookup[1]) ;
                    if $(includes) = %default
                    {
                        includes = ;
                    }
                    local library = [ ac.construct-library $(lookup[2]) :
                        [ $(property-set).refine [ property-set.create $(lookup[3]) ] ] : $(library-path) ] ;
                    configure.log-library-search-result $(name) : "yes (cached)" $(min) ;
                    return [ $(library[1]).add-raw <include>$(includes) ] $(library[2-]) ;
                }
            }
            else
            {
                local includes = [ ac.find-include-path $(property-set) : $(self.header) : $(include-path) : $(self.header-test) ] ;
                local library = [ ac.find-library $(property-set) : $(libnames) : $(library-path) ] ;
                if $(includes) && $(library)
                {
                    config-cache.set $(key) : $(includes) $(library) ;
                    if $(includes) = %default
                    {
                        includes = ;
                    }
                    library = [ ac.construct-library $(library[1]) :
                        [ $(property-set).refine [ property-set.create $(library[2]) ] ]  : $(library-path) ] ;
                    configure.log-library-search-result $(name) : "yes" $(min) ;
                    return [ $(library[1]).add-raw <include>$(includes) ] $(library[2-]) ;
                }
                else
                {
                    config-cache.set $(key) : missing ;
                    configure.log-library-search-result $(name) : "no" $(min) ;
                    return [ property-set.empty ] ;
                }
            }
        }
    }
}

class check-library-worker
{
    import property-set ;
    import targets ;
    import property ;

    rule __init__ ( target : true-properties * : false-properties * )
    {
        self.target = $(target) ;
        self.true-properties = $(true-properties) ;
        self.false-properties = $(false-properties) ;
    }

    rule check ( properties * )
    {
        local choosen ;
        local t = [ targets.current ] ;
        local p =  [ $(t).project ] ;
        local ps = [ property-set.create $(properties) ] ;
        ps = [ $(ps).propagated ] ;
        local generated =
            [ targets.generate-from-reference $(self.target) : $(p) : $(ps) ] ;
        if $(generated[2])
        {
            choosen = $(self.true-properties) ;
        }
        else
        {
            choosen = $(self.false-properties) ;
        }
        return [ property.evaluate-conditionals-in-context $(choosen) :
            $(properties) ] ;
    }
}

rule check-library ( target : true-properties * : false-properties * )
{
    local instance = [ class.new check-library-worker $(target) :
        $(true-properties) : $(false-properties) ] ;
    return <conditional>@$(instance).check
        [ property.evaluate-conditional-relevance
            $(true-properties) $(false-properties)
          : [ configure.get-relevant-features ] <link> ] ;
}
